300 3'048 Ceres laser printer; Ceres *.Pr3.Fnt and *.Lm3.Fnt files
/top 8/nohead "*"/noline "hvlrb"/col "C"/table
LineElems
Alloc
Syntax10i.Scn.Fnt
Syntax10b.Scn.Fnt
Math12.Scn.Fnt
Programming Elements for the Oberon Text System
3.0 (28 Sept 93)
C. Szyperski / M. Hausner
Introduction
This memo gives a tutorial on how to program elements for the Oberon text system. It assumes that the reader has acquired some knowledge on how to use Edit. If this is not the case it is strongly recommended to first read the Edit Guide [1].
The memo begins with an overview showing the module structure and continues with concise descriptions of the core modules. Thereafter a stepwise introduction to the programming of text elements is given. To avoid confusion it is possible to skip the module descriptions and directly start with the programming tutorial, reading the module descriptions as soon as needed.
Related documents. For a description of available commands in modules Edit and EditTools, refer to the separate memo Edit.Guide.Text [1]. For a list of currently available elements, see Elem.Guide.Text [2]. The original Edit system has been described in [3]. This report is still a useful reference when looking for the design ideas behind Edit ([4] is another, more concise reference). However, many of the details are outdated by now. The Oberon system in general is documented in [5].
[1] Edit.Guide.Text - Memo, Institute for Computer Systems, ETH Z
rich.
[2] Elem.Guide.Text - Memo, Institute for Computer Systems, ETH Z
rich.
[3] Clemens A. Szyperski. Write - An Extensible Text Editor for the Oberon System.
Technical Report 151, Dep. Informatik, ETH Z
rich, January 1991.
[4] Clemens A. Szyperski. Write_ing Applications. Designing an Extensible Text Editor as an
Application Framework. Proceedings TOOLS Europe '92, Dortmund, Germany;
Prentice Hall, Englewood Cliffs, NJ. March 1992.
[5] Martin Reiser. The Oberon System - User Guide and Programmer's Manual.
Addison_Wesley, Reading, MA. 1991.
Overview
This section introduces the modular structure of Edit and the distribution of abstractions to the various modules. Figure 1 illustrates the import relation of the Edit core modules (TextFrames, TextPrinter, ParcElems, and Texts), and the placement of typical extensions. The simpler type of extensions follows the generic Oberon model of adding new commands by implementing a new command package, i.e. a module exporting the new commands. The more involved type of extension adds new elements to the text system. Both kinds of extensions typically import all of the core modules plus any number of additionally needed other modules. (Module ParcElems is not normally required to implement a command or an element.)
Figure 1. Edit Core Modules and Placement of Typical Extensions.
Texts
Texts are generalized to be understood as sequences of attributed text_objects, where a text object is either an ordinary character or a text element. As for standard texts, the attributes are font, color, and vertical offset.
Elements behave like normal characters of ASCII code 1CX when read by a Reader or a Scanner. Additionally, the Reader field elem contains a reference to the previously read element, or NIL if a normal character was read last. Hence, programs that are not aware of elements, but are well_behaved when reading a non_graphical character of the ASCII set, can process texts containing elements. For example, a program source containing elements can be compiled, as the compiler considers all non_graphical characters as white space. Hence,
Texts.Read(R, ch);
IF R.elem # NIL THEN ...handle element...
ELSE ...handle normal character...
END;
can be used to operate on a text with embedded elements.
An element is characterized by its bounding box (W, H) in device independent coordinates. The used unit is defined as follows:
Pixel Resolution vs. Device Independent Units.
Furthermore, each element has a handler attached by means of field handle. The functionality of handlers may be compared to the frame handlers used in the Oberon viewer system. Its principles are not investigated further in this memo. By means of its handler, an element can react to messages. Texts defines the model_level messages for element filing (FileMsg), element copying (CopyMsg), and element identification (IdentifyMsg).
New elements can be written to a Buffer using procedure WriteElem. Care must be taken that a particular element is not present in more than one text at more than one position at a time. (This condition is checked and may cause a trap 99 in procedure WriteElem.) The base text of an element may be retrieved using procedure ElemBase. If the element does not belong to a text, NIL is returned. When writing an element to a text that already belongs to a text, it must be copied first. Copying is done by sending a CopyMsg to the element. The copy is returned in the message field e. (Procedure CopyElem is used to implement the copy mechanism of an element. It cannot be used to copy an element directly.)
Elements can be retrieved from a text by sequential reading (using the standard Read procedure), or by directly reading the next or previous element starting from a certain position in the text. The latter is done by calling ReadElem or ReadPrevElem, respectively.
Finally, the standard procedures Load (or Open) and Store can be used to load and store texts containing arbitrary elements. Texts uses the identification and filing messages to implement this kind of generic loading and storing. Newly created elements during load_time are transferred to Texts using variable new.
Modules that implement text elements do not register with Texts: When receiving an identify message, an element returns the module and procedure string identifying an allocation procedure; these strings are dictionary coded and written to the file. This is used upon load time (via Modules) to invoke the allocation procedure which has to allocate an element, install a handler, and assign that element to the global variable Texts.new. If this worked, a load message is sent to the newly created object; otherwise the box information is used (and the remaining element block skipped) to create an alien element. Likewise, a store message is used to ask an element to write its private data.
Procedure CopyElem copies all fields defined in the base type ElemDesc. It is supposed to be called from copy procedures defined for extended types. (A record assignment is not recommened: it copies private fields, the prev and next pointers in this case.) ElemBase makes use of the fact that elements are firmly integrated by allowing an element to find out about its current host text (or NIL if there is none).
TextFrames
TextFrames implements a frame class capable of displaying formatted texts. TextFrames defines a large number of procedures and implements many services. This section is especially terse in describing TextFrames and the reader is referred to the programming tutorial below, where TextFrames functionality is explained in more detail when needed.
TextFrames contains the screen formatter. It defines a special display prepare message (TextFrames.DisplayMsg with field prepare=TRUE) sent to elements about to be displayed. This allows for adapting to remaining space on a line and the like. Hence, an element is allowed to change its bounding box upon receipt of a display prepare message.
TextFrames tries to delegate mouse clicks to elements hit by the cursor. This is done by sending a TextFrames.TrackMsg. By tracking the mouse until all mouse buttons have been released again, the element can selectively consume mouse clicks. It is strongly recommended to restrict the set of consumed mouse clicks to middle button clicks and interclicks, i.e. the command click combinations. These are undefined for elements, anyway. Consuming left or right button clicks or interclicks causes interference with the caret and selection controlling clicks interpreted by text frames.
It is possible to attach elements to some external model, i.e. to use elements as views showing some shared model. Elements doing so may install a sub-frame by allocating a new frame of some appropriate class and assigning it to field TextFrames.DisplayMsg.elemFrame. To update these views, a model change should cause a notification by broadcasting a message to the viewer system. TextFrames delegates such messages to all installed sub-frames. A message derived from TextFrames.NotifyMsg has the additional property that the delegating frame adds its identity to the field TextFrames.NotifyMsg.frame.
Opening a TextFrame takes besides the handler to be used and the text to be displayed a bunch of additional parameters. For each of these a default variable is exported. The parameters define the border width defined around a displayed text in a text frame (left, right, top, bot), and the scroll bar width (barW, if barW > left the scroll bar is not displayed and not available).
PROCEDURE NewMenu (name, commands: ARRAY OF CHAR): Frame;
END TextFrames.
TextPrinter
TextPrinter provides functionality similar to TextFrames, but supports printing of text portions. The print message has the same fields as the display message, except for passing no frame, and for adding a field pno giving the page number of the page the element is going to be printed on. TextPrinter encapsulates the handling of printer metrics files (*.Lm3.Fnt files): A font can be registered using the procedure FontNo, returning a code number. Font may be used to lookup the font associated with a certain code. Procedure DX(f, c) returns the printer dx of character c using font f. Get(f, c, dx, x, y, w, h) returns the character metrics in global units of c using font f. Finally, GetChar(f, u, c, p, dx, x, y, w, h) returns the metrics for unit u plus the pattern p used for screen output. This last procedure is useful when formatting characters based on printer units in order to display wysiwyg strings on screen. For example, TableElems make use of this to display tables in a wysiwyg fashion.
Procedure PlaceHeader(x, y, w, pno, f, head, alt) places a header in frame (x, y, w, h), where the hight h depends on the used font f. If alt is false, or pno is odd, the page number pno is placed on the right side of the frame, and the header string head is place on the left side. Otherwise, the page number is placed on the left, and the header string on the right.
Procedure PlaceBody(x, y, w, h, T, pos, pno, place) formats a page using page number pno to fill the frame (x, y, w, h) starting at pos in text T. If place is true, the page is also sent to the printer. On return, pos indicates the first character that has not fit onto the page, i.e. the first character to appear on the next page.
DEFINITION TextPrinter; (* CAS 15-Oct-91 *)
IMPORT
Display, Fonts, Texts, TextFrames;
CONST
Unit = 3048; (*unit for a 300 dpi printer*)
TYPE
PrintMsg = RECORD (Texts.ElemMsg)
prepare: BOOLEAN;
indent: LONGINT; (*prepare => width already consumed in line, in units*)
T: Texts.Text; VAR pos: LONGINT; pno: INTEGER; place: BOOLEAN);
(*format and optionally place page with number pno in box bodyX, bodyY, bodyW, bodyH;
returns with pos pointing to the first character of the next page or to the end of the text*)
PROCEDURE PrintDraft (t: Texts.Text; header: ARRAY OF CHAR; copies: INTEGER);
END TextPrinter.
ParcElems
ParcElems implements the parcs (paragraph controls) defined in module TextFrames, adding interactive manipulation features for most parc attributes. Furthermore, the parc handler installed by ParcElems supports querying and changing parc attributes using the message ParcElems.StateMsg. This is used by the commands Edit.Set and Edit.Get. Hence, extended parcs defining new attributes can be defined that in turn extend the parameters taken by Edit.Set and Edit.Get. (The standard StyleElems take advantage of this by propagating changes to all parcs in a text that have the same name.)
DEFINITION ParcElems; (* CAS 15-Oct-91 *)
IMPORT
Display, Files, Texts, TextFrames, TextPrinter;
CONST
(*StateMsg.id*)
set = 0; get = 1;
TYPE
StateMsg = RECORD (Texts.ElemMsg)
id: INTEGER;
pos: LONGINT;
frame: TextFrames.Frame;
par: Texts.Scanner; (*used to consume set/get arguments*)
log: Texts.Text (*used to output set/get protocols*)
END;
PROCEDURE ParcExtent (T: Texts.Text; beg: LONGINT; VAR end: LONGINT);
(*returns influence interval [beg, end) of parc located in T at beg*)
(*use S to scan arguments and get selected attributes of P*)
PROCEDURE Handle (E: Texts.Elem; VAR msg: Texts.ElemMsg);
(*handler of standard parcs and its components*)
PROCEDURE Alloc;
(*allocation command called by Texts when loading a parc*)
END ParcElems.
How to Program Text Elements
This section covers the material required to implement new text elements. Starting with almost trivial functionality, new features are added stepwise. Each step is accompanied by a small but functional example in full source form. It is recommended to understand each of the steps before proceeding.
Minimal Element
A minimal element needs to handle the copy message, only. It will be displayed or printed as a white area in the text, visible only indirectly by the place it takes. When storing a text containing a minimal element, the element will not be stored (as it does not handle identification and filing messages). Hence, after reloading the text, the element is gone.
The implementation consists of only two procedures: A minimal handler to implement copying, and a command to allow elements of the new class to be inserted into texts. The handler uses Texts.CopyElem to implement the copying. If the defined elements would contain any further record fields, these should also be copied when handling a CopyMsg. The insertion procedure creates a new element and initializes the fields W, H, and handle. Then, it sends a TextFrames.InsertElemMsg to the current focus viewer. Note that the implementation of minimal elements does not even require the definition of a new type: As long as no new fields are needed, the type Texts.ElemDesc will do.
7 Texts.CopyMsg - the receiver creates a fully initialized copy of itself and returns it in the message field e.
CopyMsg = RECORD (ElemMsg)
e: Elem
END;
MODULE MinimalElems;
IMPORT
Texts, TextFrames, Oberon;
PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg);
To load and store elements it is necessary to handle identification messages and to implement an allocator for the defined elements. The identification message informs module Texts about the names of allocator procedure; these names are in turn stored in a text file. When loading a text, Texts uses the names of allocator procedures to call the allocators using Oberon.Call. An allocator in turn creates a new instance of the appropriate element class and assigns it to variable Texts.new.
7 Texts.IdentifyMsg - the receiver returns the name of its allocator (module, procedure)
IdentifyMsg = RECORD (ElemMsg)
mod, proc: ARRAY 32 OF CHAR
END;
MODULE FileableElems;
IMPORT
Texts, TextFrames, Oberon;
PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg);
The elements implemented so far had no specific state. To add state information to an element, a new type needs to be derived from Texts.ElemDesc. To enable loading and storing of that derived type, handling of another message used for filing elements has to be added. In the example of StatefulElems, a character array has been added to allow the defined elements to hold strings. Insert has been extended to take an argument and use it to initialize the newly created element.
7 Texts.FileMsg - depending on the message id the receiver loads or stores its state using the rider r. The state of the base type Texts.ElemDesc (W, H) is automatically loaded and stored. (The field pos is the position of the element in the hosting text.)
FileMsg = RECORD (ElemMsg)
id: INTEGER; (*load = 0, store = 1, other values reserved*)
pos: LONGINT;
r: Files.Rider
END;
MODULE StatefulElems;
IMPORT
Files, Texts, Oberon, TextFrames;
TYPE
Elem = POINTER TO RECORD (Texts.ElemDesc)
s: ARRAY 32 OF CHAR
END;
PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg);
Before proceeding by adding new features some remarks on the recommended structure of element implementing modules may be in order. First of all, it is a good idea to use the handler as a message dispatcher only, i.e. the handling of specific messages should be factored into a set of procedures. This makes the handler far easier to read and the implementation extensible. The latter holds if the handler and the individual handling procedures are exported: An extension implemented in a new module will define a new handler and call the old handler whenever it does not want to intercept the handling of a particular method. If it does intercept a certain message type, the extension often needs to resort to the original implementation, which it does by calling the corresponding handling procedure. Secondly, the initialization of an element should be packaged into an (possibly exported) procedure. Typically, such procedures are named Open... in the Oberon system.
The module below has exactly the same functionality as the one given in the previous section, but is structured following the conventions just explained. Also, all parts that are required to make the implementation itself extensible have been exported. (Note that for making the element type itself extensible it is necessary to define and export a named record type; this may be compared to the anonymous record type used above.)
MODULE StatefulElems;
IMPORT
Files, Texts, Oberon, TextFrames;
TYPE
Elem* = POINTER TO ElemDesc;
ElemDesc* = RECORD (Texts.ElemDesc)
s*: ARRAY 32 OF CHAR
END;
PROCEDURE^ Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg);
The elements developed so far are quite unusual as they have no visual representation. Adding visual representation is done by handling another message. This time the message is device specific (corresponds to the display) and hence is defined in module TextFrames instead of Texts. In a first step the visual representation of the element is chosen to be particularly simple: the area taken by the element is simply filled with a grey pattern.
7 TextFrames.DisplayMsg - If Xprepare, the receiver is asked to display itself (using module Display) at absolute screen coordinates (X0, Y0). The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. The hosting frame is available via frame. Field indent is not valid when Xprepare holds. Finally, an element may install a subframe by creating a new frame and returning it using field elemFrame. (This is explained in an example further below.)
DisplayMsg = RECORD (Texts.ElemMsg)
prepare: BOOLEAN;
fnt: Fonts.Font;
col: SHORTINT;
pos: LONGINT;
frame: Display.Frame;
X0, Y0: INTEGER;
indent: LONGINT;
elemFrame: Display.Frame
END;
MODULE VisibleElems;
IMPORT
Files, Display, Texts, Oberon, TextFrames;
TYPE
Elem* = POINTER TO ElemDesc;
ElemDesc* = RECORD (Texts.ElemDesc)
s*: ARRAY 32 OF CHAR
END;
PROCEDURE^ Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg);
Besides displaying an element it is useful to have a printed representation. To implement priniting it is necessary to handle the print message defined by module TextPrinter. It is very similar to the display message introduced above, but contains fewer fields, as certain features designed for interaction (like subframes) make no sense when printing. Also, the print message assumes that the receiving element uses modules Printer (and perhaps TextPrinter) instead of Display to output its representation. The example below uses a grey pattern to print itself that it similar to the one used for displaying.
7 TextPrinter.PrintMsg - If Xprepare, the receiver is asked to print itself (using module Printer) at absolute printer coordinates (X0, Y0). The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. Field indent is not valid when Xprepare holds. The number of the page that is used for the page the element will be printed on is indicated by pno.
An element may compute its bounding box instead of having it fixed. This can be used to have a printed form that has different size than the displayed form. For example, an element may be visible on screen, but invisible on paper by having a printed form of zero width and height. Also, an element may decide to fill the remaining space in a line, or all space to the next tabulator stop. All these cases are handled by implementing special reactions to the TextFrames.DisplayMsg and TextPrinter.PrintMsg when the prepare flag is set.
The example below handles the case where the width of the element is different when printing it. The Draw and Print procedures have been changed to display the string hold by the element in an underlined fashion. To do so, the procedures StrDispWidth and StrPrntWidth have been added which use information provided by modules Display and TextPrinter to compute the width of a string when displayed or printed, respectively. (Note that the width of an element is reset after printing it. This avoids problems with tools directly working on a displayed text, as such tools may inspect and use the width stored in field W of an element.)
7 TextFrames.DisplayMsg - If prepare, the receiver is asked to prepare itself for being displayed. The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. Field indent indicates the space (in units) already taken in the currently casted line. Field Y0 is set to the base line offset that will be applied to the element. (This value may be changed by the element to affect the resulting base line offset and line heights.) Fields frame, X0 and elemFrame are undefined if prepare holds. Note that the prepare message may be received more than once before the element receives the correspoding display message. This is due to line breaks caused when trying to place the element.
DisplayMsg = RECORD (Texts.ElemMsg)
prepare: BOOLEAN;
fnt: Fonts.Font;
col: SHORTINT;
pos: LONGINT;
frame: TextFrames.Frame;
X0, Y0: INTEGER;
indent: LONGINT;
elemFrame: Display.Frame
END;
7 TextPrinter.PrintMsg - If prepare, the receiver is asked to prepare itself for being printed. The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. Field indent indicates the space (in units) already taken in the currently casted line. Field Y0 is set to the base line offset that will be applied to the element. (This value may be changed by the element to affect the resulting base line offset and line heights.) The number of the page that the element is expected to be printed on is indicated by pno. Field X0 is undefined if prepare holds. Note that the prepare message may be received more than once before the element receives the correspoding print message. This is due to line and page breaks caused when trying to place the element.
The UnderlineElems developed above are already quite useful and well behaved elements. In a next step the elements are refined to react to mouse clicks. To do so, it is sufficient to handle the tracking message defined in TextFrames. The example below interprets a middle mouse click to toggle the state of the element between underlined and normal display of the encapsulated string.
7 TextFrames.TrackMsg - The receiving element, based at screen coordinates (X0, Y0) is asked to handle a mouse click at screen coordinate (X, Y) with keys pressed. The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. The hosting frame can be referred to via frame.
The final refinement of UnderlineElems introduces inplace editing, i.e. the capability to edit the contents of an element in situ. To do so in a way that follows the Oberon model, an element can install a subframe into its hosting text frame. This is done by extending the element's reaction to a DisplayMsg. Each time a display message is received the element creates a new frame, sets it up properly, and returns it in message field elemFrame. This field is initialized to NIL. If the sending text frame finds it to be non_NIL on return, it installs the passed frame into its descender list of frames. Then the user may choose to use a left mouse click to focus one of the installed subframes. A focussed subframe is framed using a grey pattern to make the focus state clearly visible. To control the process of focussing and de_focussing, an element can interpret the TextFrames.FocusMsg.
A focussed subframe receives all messages broadcasted to the viewer system or sent to the hosting text frame, unless intercepted by the text frame. Hence, any existing frame implementation can be used. (Note: TextFrames currently does not support nesting - hence a text frame currently cannot be installed as a subframe.) Also, a minimalistic frame holding only a very simple handler can be used to implement elements that need to receive broadcast messages when visible. This can be used to implement active elements by having a task broadcast a special message, say once a second. All visible elements that have installed a subframe will receive this message, and - if they know the message type - can react to it. For example, the ClockElems and IconElems modules work this way. The example below uses a minimal frame to receive broadcast messages for simple animation purposes.
7 TextFrames.FocusMsg - The receiver's subframe is about to be focussed or defocussed, depending on the value of focus. The subframe is indicated by elemFrame, the host frame by frame.